global without sharing class ProcessRecords implements Database.batchable<sObject> {
    
    public Boolean isTest {get;set;}
    public Boolean isDirect {get;set;}
    public List<sObject> objsList;
    
    public ProcessRecords(List<sObject> objs, Boolean isTestParam, Boolean isDirectParam) {
        objsList = objs;
        isTest = isTestParam;
        isDirect = isDirectParam;
    }
    
    global List<sObject> start(Database.BatchableContext info){
        return objsList;
    }
    global void execute(Database.BatchableContext info, List<sObject> scope){
        if (scope == null) scope = new List<sObject>();
        this.processObjects(scope,isTest);
    }
    global void finish(Database.BatchableContext info){
        
    }
    
    public void processObjects(List<sObject> records, Boolean isTest) {
        List<String> ownerIds = new List<String>();
        List<GroupMember> groupMembersList;
        
        List<QueueSetting__c> qsList;
        Map<Id,QueueSetting__c> QueueSettingsToOwnerId = new Map<Id,QueueSetting__c>();
        List<Id> userIdList = new List<Id>();
        List<User> userList;
        Map<Id,User> userToId = new Map<Id,User>();
        
        if (records.size() > 0) {
            /* Number of triggers running in this time */
            List<CronTrigger> triggers = [SELECT id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger limit 100];
            Integer numOfTriggers = triggers.size();
            
            /** Prepare records to Bulk operations */
            for (sObject item: records) {
                String currId = (String)item.get('OwnerId');
                ownerIds.add(currId.substring(0,15));
            }
            
            groupMembersList = [select Id,GroupId,UserOrGroupId from GroupMember where GroupId in :ownerIds];
            if (groupMembersList.size() > 0) {
                for (GroupMember item: groupMembersList) {
                    userIdList.add(item.UserOrGroupId);
                }
            }
            List<Group> groups = [select Id,RelatedId from Group where Id = :userIdList];
            List<Id> roleIds = new List<Id>();
            Map<Id,Id> roleIdToGroupId = new Map<Id,Id>();
            for (Group item: groups) {
                roleIds.add(item.RelatedId);
                roleIdToGroupId.put(item.Id,item.RelatedId);
            }
            
            /* QueueSettings map */
            qsList = [select Id,QueueId__c,Active__c,AssignmentType__c
                       from QueueSetting__c 
                       where QueueId__c in :ownerIds];
            if (qsList.size() > 0) {
                for (QueueSetting__c item: qsList) {
                    QueueSettingsToOwnerId.put(item.QueueId__c,item);
                }
            }
            /* Users map */
            userList = [select Id,OOOStartDate__c,OOOEndDate__c,Workingdays__c,Name,MaximumAllowedLeadAssignments__c,MaximumAllowedCaseAssignments__c,StartDay__c,EndDay__c,LastAssigned__c,UserRoleId
                        from User
                        where ((Id in :userIdList) or (UserRoleId in :userIdList) or (UserRoleId in :roleIds))];
            String userListIdsStr = '';
            if (userList.size() > 0) {
                for (User item: userList) {
                    userToId.put(item.Id,item);
                    userListIdsStr += ', \'' + item.Id + '\'';
                }
                if (userListIdsStr != '') {
                    userListIdsStr = userListIdsStr.substring(2);
                }
            }
            /* Assignment custom settings to sObject type */
            Map<String,EZAssignSettings__c> assignmentSettingsToObjType = new Map<String,EZAssignSettings__c>();
            List<EZAssignSettings__c> assSetList = [Select ObjectAPIName__c, ActiveFieldValues__c, ActiveFieldAPIName__c From EZAssignSettings__c limit 1000];
            for (EZAssignSettings__c item: assSetList) {
                assignmentSettingsToObjType.put(item.ObjectAPIName__c,item);
            }
        
            /* Get count of Leads assigned to users */
            String activeValue;
            Integer intPart;
            Decimal decPart;
            Boolean isInt, isDec;
            String leadsToUsersQueryStr = 'select OwnerId, count(Name) cnt from Lead ';
            if (assignmentSettingsToObjType.get('Lead') != null) {
                activeValue = assignmentSettingsToObjType.get('Lead').ActiveFieldValues__c;
                isInt = false;
                isDec = false;
                
                try {
                    decPart = Decimal.valueOf(activeValue);
                    if ((decPart + '') == activeValue) {
                        isDec = true;
                    }
                } catch (Exception ex) {
                    
                }
                
                leadsToUsersQueryStr += ' where ';
                List<String> valueParts = activeValue.split(',');
                String valuesFilter = '';
                for (String val: valueParts) {
                    if (valuesFilter != '') {
                        valuesFilter += 'or';
                    }
                    String valToIns = val;
                    if (val != 'true' && val != 'false' && !isDec) {
                        valToIns = '\'' + valToIns + '\'';
                    }
                    valuesFilter += ' (' + assignmentSettingsToObjType.get('Lead').ActiveFieldAPIName__c + ' = ' + valToIns + ') ';
                }
                if (valuesFilter != '') {
                    leadsToUsersQueryStr += '( ' + valuesFilter + ' ) ';
                }
            }
            leadsToUsersQueryStr += ' AND (OwnerId in (' + userListIdsStr + ')) ';
            leadsToUsersQueryStr += ' group by OwnerId limit 20';
            System.debug('xxxxxxxxxxxx' + leadsToUsersQueryStr);
            System.debug('xxxxxxxxxxxx' + limits.getQueryRows());
            List<AggregateResult> leadsToUsersCountList = database.query(leadsToUsersQueryStr);
            System.debug('xxxxxxxxxxxx' + limits.getQueryRows());
            Map<Id,Integer> usersToLeadsCount = new Map<Id,Integer>();
            for (AggregateResult item: leadsToUsersCountList) {
                usersToLeadsCount.put((Id)item.get('OwnerId'),(Integer)item.get('cnt'));
            }
            
            /* Get count of Cases assigned to users */
            String casesToUsersQueryStr = 'select OwnerId, count(Id) cnt from Case';
            if (assignmentSettingsToObjType.get('Case') != null) {
                activeValue = assignmentSettingsToObjType.get('Case').ActiveFieldValues__c;
                isInt = false;
                isDec = false;
                
                try {
                    decPart = Decimal.valueOf(activeValue);
                    if ((decPart + '') == activeValue) {
                        isDec = true;
                    }
                } catch(Exception ex) {
                    
                }
                
                casesToUsersQueryStr += ' where ';
                List<String> valueParts = activeValue.split(',');
                String valuesFilter = '';
                for (String val: valueParts) {
                    if (valuesFilter != '') {
                        valuesFilter += 'or';
                    }
                    String valToIns = val;
                    if (val != 'true' && val != 'false' && !isDec) {
                        valToIns = '\'' + valToIns + '\'';
                    }
                    valuesFilter += ' (' + assignmentSettingsToObjType.get('Case').ActiveFieldAPIName__c + ' = ' + valToIns + ') ';
                }
                if (valuesFilter != '') {
                    casesToUsersQueryStr += ' ( ' + valuesFilter + ' ) ';
                }
                
            }
            casesToUsersQueryStr += ' AND (OwnerId in (' + userListIdsStr + ')) ';
            casesToUsersQueryStr += ' group by OwnerId limit 20000';
            List<AggregateResult> casesToUsersCountList = database.query(casesToUsersQueryStr);
            
            Map<Id,Integer> usersToCasesCount = new Map<Id,Integer>();
            for (AggregateResult item: casesToUsersCountList) {
                usersToCasesCount.put((Id)item.get('OwnerId'),(Integer)item.get('cnt'));
            }
            
            List<EZAssignSchedulerSettings__c> schedulerCustomSettings = [select Name,MinutestoStart__c from EZAssignSchedulerSettings__c limit 1];
            
            /** Process records */
            for (sObject record: records) {
                if (QueueSettingsToOwnerId.get((String)record.get('OwnerId')) != null  || isTest) {
                    String assignmentType;
                    if (isTest) {
                        assignmentType = 'Round Robin';
                    } else {
                        assignmentType = QueueSettingsToOwnerId.get((String)record.get('OwnerId')).AssignmentType__c;
                    }
                    if (assignmentType == 'Load Balanced' || assignmentType == 'Round Robin') { //if owner a member of RR or LB group
                        
                        /* START PROCESS */
                        // ==== Get members not on vacation and with working hours ==== //
                        List<User> allGroupMembers = new List<User>();
                        
                        String queueId = (String)record.get('OwnerId');
                        for (GroupMember item: groupMembersList) {
                            if (item.GroupId == queueId) {
                                User currentUser = UserToId.get(item.UserOrGroupId);
                                String startDay = '0';
                                String endDay = '0';
                                if (currentUser != null) {
                                    if (currentUser.StartDay__c != null) {
                                        startDay = currentUser.StartDay__c;
                                    }
                                    if (currentUser.EndDay__c != null) {
                                        endDay = currentUser.EndDay__c;
                                    }
                                    String workingDays = currentUser.Workingdays__c;
                                    if (!(DateTime.now() > currentUser.OOOStartDate__c && // Check users to vacation
                                            DateTime.now() < currentUser.OOOEndDate__c) &&
                                            Integer.valueOf(DateTime.now().format('H')) >= Integer.valueOf(startDay) && // Check users to working time
                                            Integer.valueOf(DateTime.now().format('H')) < Integer.valueOf(endDay) ||
                                            isTest
                                            )
                                    {
                                        if (workingDays != null) {
                                            if (workingDays.contains(DateTime.now().format('E'))) { 
                                                allGroupMembers.add(currentUser);
                                            }
                                        }
                                        if (isTest) {
                                            allGroupMembers.add(currentUser);
                                        }
                                    }
                                } else {
                                    for (User subItem: userList) {
                                        if (subItem.UserRoleId == roleIdToGroupId.get(item.UserOrGroupId)) {
                                            currentUser = UserToId.get(subItem.Id);
                                            startDay = '0';
                                            endDay = '0';
                                            if (currentUser.StartDay__c != null) {
                                                startDay = currentUser.StartDay__c;
                                            }
                                            if (currentUser.EndDay__c != null) {
                                                endDay = currentUser.EndDay__c;
                                            }
                                            if (currentUser != null) {
                                                String workingDays = currentUser.Workingdays__c;
                                                if (!(DateTime.now() > currentUser.OOOStartDate__c && // Check users to vacation
                                                        DateTime.now() < currentUser.OOOEndDate__c) &&
                                                        Integer.valueOf(DateTime.now().format('H')) >= Integer.valueOf(startDay) && // Check users to working time
                                                        Integer.valueOf(DateTime.now().format('H')) < Integer.valueOf(endDay) ||
                                                        isTest
                                                        )
                                                {
                                                    if (workingDays != null) {
                                                        if (workingDays.contains(DateTime.now().format('E'))) { 
                                                            allGroupMembers.add(currentUser);
                                                        }
                                                    }
                                                    if (isTest) {
                                                        allGroupMembers.add(currentUser);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        
                        // ==== If group members found ==== //
                            
                            User userToAssign;
                            if (isTest) {
                                userToAssign = new User(Id = UserInfo.getUserId());
                            }
                            // ==== If this queue a Round Robin ==== //
                            if (assignmentType == 'Round Robin' || isTest) {
                                // ==== Find User with min Last Assignment date/time ==== //
                                DateTime minDT = DateTime.now();
                                for (User groupMember: allGroupMembers) { // Scaning
                                    Integer recordsToUserCount = 0;
                                    Double maxAllowed = 0;
                                    
                                    if (usersToLeadsCount.get(groupMember.Id) != null && '' + record.getSObjectType() == 'Lead') { 
                                        recordsToUserCount += usersToLeadsCount.get(groupMember.Id);
                                    }
                                    if (usersToCasesCount.get(groupMember.Id) != null && '' + record.getSObjectType() == 'Case') { 
                                        recordsToUserCount += usersToCasesCount.get(groupMember.Id);
                                    }
                                    if ('' + record.getSObjectType() == 'Lead') {
                                        maxAllowed = groupMember.MaximumAllowedLeadAssignments__c;
                                    }
                                    if ('' + record.getSObjectType() == 'Case') {
                                        system.debug('==== Curr usr ==== max cases: ' + groupMember.MaximumAllowedCaseAssignments__c + ' >>>>');
                                        maxAllowed = groupMember.MaximumAllowedCaseAssignments__c;
                                    }
                                    
                                    if (recordsToUserCount < maxAllowed && maxAllowed != null && maxAllowed != 0) {
                                        if (userToId.get(groupMember.Id).LastAssigned__c < minDT ||
                                            userToId.get(groupMember.Id).LastAssigned__c == null)
                                        {
                                            minDT = userToId.get(groupMember.Id).LastAssigned__c;
                                            userToAssign = groupMember;
                                        }
                                    }
                                }
                            }
                            
                            // ==== If this queue a Load Balanced ==== //
                            if (assignmentType == 'Load Balanced' || isTest) {
                                Integer minCount = 1000;
                                for (User groupMember: allGroupMembers) {
                                    Integer recordsToUserCount = 0;
                                    Double maxAllowed = 0;
                                    
                                    if (usersToLeadsCount.get(groupMember.Id) != null && '' + record.getSObjectType() == 'Lead') { 
                                        recordsToUserCount += usersToLeadsCount.get(groupMember.Id);
                                    }
                                    if (usersToCasesCount.get(groupMember.Id) != null && '' + record.getSObjectType() == 'Case') { 
                                        recordsToUserCount += usersToCasesCount.get(groupMember.Id);
                                    }
                                    if ('' + record.getSObjectType() == 'Lead') {
                                        maxAllowed = groupMember.MaximumAllowedLeadAssignments__c;
                                    }
                                    if ('' + record.getSObjectType() == 'Case') {
                                        maxAllowed = groupMember.MaximumAllowedCaseAssignments__c;
                                    }
                                    
                                    if (recordsToUserCount < maxAllowed && maxAllowed != null && maxAllowed != 0 || isTest) {
                                        if (recordsToUserCount < minCount) {
                                            minCount = recordsToUserCount;
                                            userToAssign = groupMember;
                                        } else if (recordsToUserCount == minCount) {
                                            if (userToAssign == null) userToAssign = groupMember;
                                            if (userToId.get(groupMember.Id).LastAssigned__c <
                                                userToId.get(userToAssign.Id).LastAssigned__c
                                            ) {
                                                userToAssign = groupMember;
                                            }
                                        }
                                    }
                                }
                            }
                            
                            // ==== Assign Object to User ==== //
                            if (userToAssign != null) { // If user to assign found
                                record.put('OwnerId',userToAssign.Id);
                                userToAssign.LastAssigned__c = DateTime.now();
                                update userToAssign;
                                if (''+record.getSObjectType() == 'Lead') {
                                    if (usersToLeadsCount.get(userToAssign.Id) == null || usersToLeadsCount.get(userToAssign.Id) == 0) {
                                        usersToLeadsCount.put(userToAssign.Id,1);
                                    } else {
                                        usersToLeadsCount.put(userToAssign.Id,usersToLeadsCount.get(userToAssign.Id)+1);
                                    }
                                }
                                if (''+record.getSObjectType() == 'Case') {
                                    if (usersToCasesCount.get(userToAssign.Id) == null || usersToCasesCount.get(userToAssign.Id) == 0) {
                                        usersToCasesCount.put(userToAssign.Id,1);
                                    } else {
                                        usersToCasesCount.put(userToAssign.Id,usersToCasesCount.get(userToAssign.Id)+1);
                                    }
                                    
                                }
                                
                                // ============== Udate records owners =============== //
                                if (!isDirect) {
                                    update record;
                                }
                            } else { // Else create scheduled task through a hour to process this record
                                    if (schedulerCustomSettings.size() > 0 || isTest) {
                                        Integer minutesToNextRun = schedulerCustomSettings[0].MinutestoStart__c.intValue();
                                        Integer currentMinutes = Integer.valueOf(DateTime.now().format('m')) + minutesToNextRun;
                                        
                                        if (currentMinutes >= 60) {
                                            currentMinutes -= 60;
                                        }
                                        
								        List<EZAssignJobIDs__c> jobRecords = [select JobID__c, Name from EZAssignJobIDs__c where Name = 'Load Balanced Scheduler' limit 1];
								        EZAssignJobIDs__c jobRecord;
								        if (jobRecords.size() > 0) {
								        	jobRecord = jobRecords[0];
								        } else {
								        	jobRecord = new EZAssignJobIDs__c();
								            jobRecord.Name = 'Load Balanced Scheduler';
								            jobRecord.JobID__c = '000000000000000';
								        }
								        List<CronTrigger> scheduledJobs = [select Id from CronTrigger where Id = :jobRecord.JobID__c];
								        
								        if (scheduledJobs.size() == 0) {
								        	String jobId = System.schedule('Load Balanced Scheduler', '0 ' + currentMinutes + ' * * * ?', new SchedulerClass());
								            jobRecord.JobID__c = jobId;
								            upsert jobRecord;
								        }
                                            
                                    }
                            }
                        
                    }
                }
            }
        }
    }
    
}